/**
 * Copyright (c) 2012 Nokia Corporation. 
 */

package com.nokia.example.tictactoe.engine;


/**
 * The game model responsible for dealing the game logic.
 */
public class GameModel {
	private static GameModel _instance = null; // Singleton instance
	
	// Constants
	public static final int DEFAULT_GRID_SIZE_SQUARED = 3;
	public static final int DEFAULT_MARKS_REQUIRED_TO_WIN = 3;
	public static final int MARK_EMPTY = 0;
	public static final int MARK_X = 1;
	public static final int MARK_O = 2;
	private static final int MAX_LISTENERS = 3;
	
	// Members
	private int _gridSizeSquared = DEFAULT_GRID_SIZE_SQUARED;
	private int _gridSize = DEFAULT_GRID_SIZE_SQUARED * DEFAULT_GRID_SIZE_SQUARED;
	private int _marksRequiredToWin = DEFAULT_MARKS_REQUIRED_TO_WIN;
	private int[] _grid;
	private IGameModelListener[] _listeners;
	private int[] _winningMarksX;
	private int[] _winningMarksO;
	private int _whoseTurn;
	private int _playerMark;
	private int _winner;
	
	/**
	 * Getter for the game model singleton instance.
	 * @param listener A game model listener. Can be null.
	 * @return The singleton instance.
	 */
	public static final GameModel instance(IGameModelListener listener) {
		if (_instance == null) {
			_instance = new GameModel(listener);
		}
		else if (listener != null) {
			_instance.addListener(listener);
		}
		
		return _instance;
	}
	
	/**
	 * Constructor.
	 * @param listener Game model listener or null if none.
	 */
	private GameModel(IGameModelListener listener) {
		// Create and initialize the game grid
		_listeners = new IGameModelListener[MAX_LISTENERS];
		initModel(); // Must be called the first time before any listeners are added
		
		for (int i = 0; i < MAX_LISTENERS; ++i) {
			_listeners[i] = null;
		}
		
		_listeners[0] = listener;
	}
	
	/**
	 * Adds the given listener.
	 * @param listener The listener to add.
	 * @return True if successful, false otherwise.
	 */
	public boolean addListener(final IGameModelListener listener) {
		for (int i = 0; i < MAX_LISTENERS; ++i) {
			if (_listeners[i] == null) {
				_listeners[i] = listener;
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Removes the given listener.
	 * @param listener The listener to remove.
	 * @return True if successful, false otherwise.
	 */
	public boolean removeListener(final IGameModelListener listener) {
		for (int i = 0; i < MAX_LISTENERS; ++i) {
			if (_listeners[i] == listener) {
				_listeners[i] = null;
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Sets the new game specifications and re-initializes the model.
	 * @param gridSizeSquared The size of the grid squared.
	 * @param marksRequiredToWin The number of marks in a row required to win.
	 */
	public void setSpecs(final int gridSizeSquared,
					     final int marksRequiredToWin)
	{
		System.out.println("GameModel::setSpecs(): "
				+ gridSizeSquared + ", " + marksRequiredToWin);
		
		if (gridSizeSquared < 1 || marksRequiredToWin > gridSizeSquared) {
			throw new IllegalArgumentException();
		}
		
		_gridSizeSquared = gridSizeSquared;
		_gridSize = _gridSizeSquared * _gridSizeSquared;
		_marksRequiredToWin = marksRequiredToWin;
		
		initModel();
		
		// Notify the listeners
		for (int i = 0; i < MAX_LISTENERS; ++i) {
			if (_listeners[i] != null) {
				_listeners[i].onGameSpecificationsChanged();
			}
		}
	}
	
	/** 
	 * @return The size of the grid squared.
	 */
	public final int gridSizeSquared() {
		return _gridSizeSquared;
	}
	
	/** 
	 * @return Marks required to win.
	 */
	public final int marksRequiredToWin() {
		return _marksRequiredToWin;
	}
	
	/**
	 * Initializes the game grid and notifies the listener.
	 */
	public void initModel() {
		System.out.println("GameModel::initModel()");
		
		if (_grid != null) {
			_grid = null;
			_winningMarksX = null;
			_winningMarksO = null;
		}
		
		_grid = new int[_gridSize];
		_winningMarksX = new int [_marksRequiredToWin * 2];
		_winningMarksO = new int [_marksRequiredToWin * 2];
		
		_winner = -1;
		
		for (int i = 0; i < _gridSize; ++i) {
			_grid[i] = MARK_EMPTY;
		}

		_whoseTurn = MARK_X;
		_playerMark = MARK_EMPTY;		
		
		for (int i = 0; i < MAX_LISTENERS; ++i) {
			if (_listeners[i] != null) {
				_listeners[i].onGameModelChanged();
			}
		}
	}
	
	/**
	 * @return True if the grid is empty, false otherwise.
	 */
	public final boolean gridIsEmpty() {
		for (int i = 0; i < _gridSize; ++i) {
			if (_grid[i] != MARK_EMPTY) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * @return The grid array.
	 */
	public final int[] grid() {
		return _grid;
	}
	
	/**
	 * @param x X coordinate of the grid.
	 * @param y Y coordinate of the grid.
	 * @return The mark at the given coordinates.
	 * @throws IllegalArgumentException
	 */
	public final int markAt(int x, int y) throws IllegalArgumentException {
		final int index = y * _gridSizeSquared + x;
		
		if (index < 0 || index >= _gridSize) {
			throw new IllegalArgumentException();
		}
		
		return _grid[index]; 
	}
	
	/**
	 * Sets mark in the grid to the given mark and notifies a possible listener.
	 * Note that this method does no sanity check regarding whose turn it is or
	 * whether the given position already contains a mark or not.
	 * @param x X coordinate.
	 * @param y Y coordinate.
	 * @param mark The mark to set.
	 * @throws IllegalArgumentException
	 */
	public void setMark(final int x, final int y, final int mark)
			throws IllegalArgumentException
	{
		final int index = y * _gridSizeSquared + x;
		
		if (index < 0 || index >= _gridSize || mark < MARK_EMPTY || mark > MARK_O) {
			throw new IllegalArgumentException();
		}
		
		_grid[index] = mark;

		// Notify the listeners
		for (int i = 0; i < MAX_LISTENERS; ++i) {
			if (_listeners[i] != null) {
				_listeners[i].onGameModelChanged();
			}
		}
		
		// Check if the game ended
		int outcome = checkForGameOver();
		
		if (outcome != -1) {
			// Game ended! Notify the listeners.
			_winner = outcome;
			
			for (int i = 0; i < MAX_LISTENERS; ++i) {
				if (_listeners[i] != null) {
					_listeners[i].onGameOver(outcome);
				}
			}
		}
	}
	
	/**
	 * @return Integer value (MARK_X or MARK_O) indicating whose turn it is.
	 */
	public final int whoseTurn() {
		return _whoseTurn;
	}
	
	/**
	 * Sets the turn.
	 * @param mark Whose turn it should be (MARK_X or MARK_O).
	 */
	public void setWhoseTurn(final int mark) {
		if (mark == MARK_EMPTY || mark == MARK_X || mark == MARK_O) {
			_whoseTurn = mark;
			
			for (int i = 0; i < MAX_LISTENERS; ++i) {
				if (_listeners[i] != null) {
					_listeners[i].onWhoseTurnChanged(_whoseTurn);
				}
			}
		}
	}
	
	/**
	 * Switches turns.
	 * @return Value indicating whose turn it now is.
	 */
	public int switchTurns() {
		if (_whoseTurn == MARK_O) {
			setWhoseTurn(MARK_X);
		}
		else {
			setWhoseTurn(MARK_O);
		}
		
		return _whoseTurn;
	}
	
	/**
	 * Returns the mark of the player.
	 * @return The mark of the player or GameModel.MARK_EMPTY if not set.
	 */
	public final int playerMark() {
		return _playerMark;
	}
	
	/**
	 * Sets the player mark.
	 * @param mark
	 */
	public void setPlayerMark(final int mark) {
		System.out.println("GameModel::setPlayerMark(): " + mark);
		
		if (mark == MARK_EMPTY || mark == MARK_X || mark == MARK_O) {
			_playerMark = mark;
		}		
	}
	
	/** 
	 * @return The winner (MARK_X or MARK_O) or -1 if no winner.
	 */
	public final int winner() {
		return _winner;
	}
	
	/** 
	 * @return An array of coordinates of the winning marks or an empty array
	 * 		   if no winner exists.
	 */
	public final int[] winningMarks()
	{
		if (_winner == MARK_X) {
			return _winningMarksX;
		}
		
		if (_winner == MARK_O) {
			return _winningMarksO;
		}
		
		return new int[_marksRequiredToWin * 2]; 
	}
	
	/**
	 * Checks the grid for game ending scenarios.
	 * @return -1 if the game is still ongoing, mark of the winner (MARK_X or
	 * 	  	   MARK_O) or MARK_EMPTY in case of a tie.
	 * @throws Exception 
	 */
	private final int checkForGameOver() {
		int XCount = 0;
		int OCount = 0;
		int tempMark;
		boolean emptySpotFound = false;
		
		// Horizontal lines 
		for (int y = 0; y < _gridSizeSquared; ++y) {
			for (int x = 0; x < _gridSizeSquared; ++x) {
				tempMark = _grid[y * _gridSizeSquared + x];
				
				if (tempMark == MARK_X) {
					if (OCount > 0) {
						OCount = 0;
					}
					
					_winningMarksX[XCount * 2] = x;
					_winningMarksX[XCount * 2 + 1] = y;
					XCount++;
				}
				else if (tempMark == MARK_O) {
					if (XCount > 0) {
						XCount = 0;
					}
					
					_winningMarksO[OCount * 2] = x;
					_winningMarksO[OCount * 2 + 1] = y;
					OCount++;
				}
				else {
					// Empty
					emptySpotFound = true;
					XCount = 0;
					OCount = 0;
				}
				
				if (XCount == _marksRequiredToWin) {
					return MARK_X;
				}
				else if (OCount == _marksRequiredToWin) {
					return MARK_O;
				}
			}
			
			XCount = 0;
			OCount = 0;
		}

		// Check for tie
		if (emptySpotFound == false) {
			// No empty spots. Thus, it's a tie.
			return MARK_EMPTY;
		}
		
		// Vertical lines 
		for (int x = 0; x < _gridSizeSquared; ++x) {
			for (int y = 0; y < _gridSizeSquared; ++y) {
				tempMark = _grid[y * _gridSizeSquared + x];
				
				if (tempMark == MARK_X) {
					if (OCount > 0) {
						OCount = 0;
					}
					
					_winningMarksX[XCount * 2] = x;
					_winningMarksX[XCount * 2 + 1] = y;					
					XCount++;
				}
				else if (tempMark == MARK_O) {
					if (XCount > 0) {
						XCount = 0;
					}
					
					_winningMarksO[OCount * 2] = x;
					_winningMarksO[OCount * 2 + 1] = y;
					OCount++;
				}
				else {
					// Empty
					XCount = 0;
					OCount = 0;
				}
				
				if (XCount == _marksRequiredToWin) {
					return MARK_X;
				}
				else if (OCount == _marksRequiredToWin) {
					return MARK_O;
				}
			}
			
			XCount = 0;
			OCount = 0;
		}
		
		// Diagonal, top-left to bottom-right
		int tempX = _gridSizeSquared - _marksRequiredToWin;
		int tempY = 0;
		
		if (tempX < 0) {
			// The game cannot be won!!!
			return MARK_EMPTY;
		}
		
		int x;
		int y;

		while (tempY <= _gridSizeSquared - _marksRequiredToWin) {
			x = tempX;
			y = tempY;
			
			while (x < _gridSizeSquared && y < _gridSizeSquared) {
				tempMark = _grid[y * _gridSizeSquared + x];
				
				if (tempMark == MARK_X) {
					if (OCount > 0) {
						OCount = 0;
					}
					
					_winningMarksX[XCount * 2] = x;
					_winningMarksX[XCount * 2 + 1] = y;
					XCount++;
				}
				else if (tempMark == MARK_O) {
					if (XCount > 0) {
						XCount = 0;
					}
					
					_winningMarksO[OCount * 2] = x;
					_winningMarksO[OCount * 2 + 1] = y;
					OCount++;
				}
				else {
					// Empty
					XCount = 0;
					OCount = 0;
				}
				
				if (XCount == _marksRequiredToWin) {
					return MARK_X;
				}
				else if (OCount == _marksRequiredToWin) {
					return MARK_O;
				}
				
				x++;
				y++;				
			}
			
			XCount = 0;
			OCount = 0;
			
			if (tempX > 0) {
				tempX--;
			}
			else {
				tempY++;
			}
		}
		
		tempX = _gridSizeSquared - 1;
		tempY = _gridSizeSquared - _marksRequiredToWin;
		
		// Diagonal, top-right to bottom-left
		while (tempX >= _marksRequiredToWin - 1) {
			x = tempX;
			y = tempY;
			
			while (x >= 0 && y < _gridSizeSquared) {
				tempMark = _grid[y * _gridSizeSquared + x];
				
				if (tempMark == MARK_X) {
					if (OCount > 0) {
						OCount = 0;
					}
					
					_winningMarksX[XCount * 2] = x;
					_winningMarksX[XCount * 2 + 1] = y;
					XCount++;
				}
				else if (tempMark == MARK_O) {
					if (XCount > 0) {
						XCount = 0;
					}
					
					_winningMarksO[OCount * 2] = x;
					_winningMarksO[OCount * 2 + 1] = y;
					OCount++;
				}
				else {
					// Empty
					XCount = 0;
					OCount = 0;
				}
				
				if (XCount == _marksRequiredToWin) {
					return MARK_X;
				}
				else if (OCount == _marksRequiredToWin) {
					return MARK_O;
				}
				
				x--;
				y++;				
			}
			
			XCount = 0;
			OCount = 0;
			
			if (tempY > 0) {
				tempY--;
			}
			else {
				tempX--;
			}
		}
		
		return -1; // Game still ongoing
	}
}
